package peerToPeerFacebook.proj;

import java.io.Serializable;
import java.lang.reflect.Method;

import peerToPeerFacebook.src.edu.washington.cs.cse490h.lib.Callback;

/** RPC Client object. This object should be used to invoke methods on remote server. Each RPCClient object is attached
 * to one and only one server. Connecting for different servers requires multiple objects. However, for the same
 * server same RPCClient object should be used. 
 */
public final class RPCClient extends RPCBase implements RemoteInvoker
{
	/** ID of the remote server that the client is working with. */
	private int remoteServerID;
	
	protected static final int TIMEOUT = 10;
	
	protected class RemoteCall
	{
		public long sequenceID = -1;
		public IResultCallback resultCallback = null;				
		public RPCInvokePacket invokePacket = null;
		public int retries = 0;
	}
	
	public int remoteServerID()
	{
		return remoteServerID;
	}
	
	/** outstanding call */
	protected RemoteCall outstandingCall = null; 
		
	/** Constructs the object and associates with given remote server.
	 * 
	 * @param node Node reference that should be used for network communication
	 * @param remoteServerID Remote server ID
	 */
	public RPCClient(INode node, int remoteServerID)
	{
		super(node);	
		ArgumentValidator.IsGreaterThanOrEqualTo("remoteServerID", remoteServerID, 0);		
		this.remoteServerID = remoteServerID;
	}
	
	/** Invokes method on remote server. This method returns immediately BEFORE the function is executed on remote.
	 * However, client should pass in Result callback object that will be triggered when client decides that sever
	 * finished execution of the call.
	 * 
	 * @note In this case we choose to implement non-blocking invoke method. For real RPC support invoke method should
	 * 	block until server finishes function execution. The main motivator for not using blocking call is that blocking
	 *  here would block whole simulation (including packet delivery). It is possible to have a requirement that this
	 *  method should be called from thread that is not running simulation. As description of the problem clearly states
	 *  that we should not implement multithreaded environment we chose not go this route.
	 *  
	 *  Having Said that it is easy to see how this method can be changed to blocking one. For example, before return 
	 *  from this method we could wait on event that would be signaled from Result callback.
	 * 
	 *  Here is example that you can use. Suppose client would like to invoke two methods, method1 and method2 on server
	 *  then the client should use code similar to this:
	 * 	invoke("method1", new Serializable[] { }, new IResult() {
	 *		public void result(Status status, Serializable result)
	 *		{
	 *			if(status == Status.Success)
	 *			{
	 *				// In case remote function returned some value, it is stored in result parameter
	 *				invoke("method2", new Serializable[] {}, new IResult() {
	 *					public void result(Status status, Serializable result)
	 *					{
	 *						if(status == Status.Success)
	 *						{ } // call succeeded
	 *						else
	 *						{ } // call failed
	 *					}
	 *				});
	 *			} else {
	 *				// handle failure
	 *			}
	 *		}
	 *	});
     *
	 * 
	 * @param method
	 * @param params
	 * @param resultCallback
	 */
	@Override
    public void invoke(String method, Serializable[] params, IResultCallback resultCallback)
	{
		assert outstandingCall == null : "Only one call at the time is supported. Call to server " + remoteServerID + 
				" with ID is still pending: " + outstandingCall.sequenceID;
		
		// First we need to know what sequence number we should use to talk to server.
		Long sequenceID;
		sequenceID = readFromDisk(getServerStorageName(remoteServerID));

		// It does not seem to make sense to propagate random exception to caller. So we gather them all and report
		// back to client. If we can't figure out what to do here, then RPC layer is probably hosed.
		try
		{			
			// it is fine if ID is zero - means we have not chat with this server yet
			if (sequenceID == null)
				sequenceID = 0L;
			
			// Get new ID and make sure it is persistent in case we crash or something.
			// From this point on, we will not be using same generation ID even after the crash. However, it is worth to
			// note that we would also not re-send the query if we crash. 
			sequenceID++;
			writeToDisk(getServerStorageName(remoteServerID), sequenceID);
			
			// generate call information
			outstandingCall = new RemoteCall();
			outstandingCall.sequenceID = sequenceID;
			outstandingCall.resultCallback = resultCallback;
			
			// Create packet that we will be using for the call
			outstandingCall.invokePacket = new RPCInvokePacket(sequenceID, method, params);
					
			sendCall(outstandingCall.sequenceID);
		} catch (Exception e) {
			// This does not mean fatal end to the server yet. So we can pass it to the caller
			notifyCaller(Status.Exception, new RPCError("Failed to invoke call", e));
		}
	}
	
	/** Sends the call to the server and schedules callback to make sure that the call is executed.
	 * 
	 * @param forSequanceID Expected sequence ID of the call. It is possible that this function is triggered as part of
	 * 	callback. However, by that time, the original request may have already been acknowledged and new one started.
	 * 	In that case the callback should ignore call.
	 * @throws RPCException
	 */
	public void sendCall(Long forSequanceID) throws RPCException
	{
		// This may be called from the timeout callback. It is possible that at this time we already received reply
		// from the server. So there is not need to send any packets or register more callback.
		if (outstandingCall == null)
			return;		// we are done with this call.
		
		// make sure this is callback for current call and not future one
		assert outstandingCall.sequenceID >= forSequanceID: "Sequence ID to send is higher the we area dealing with";
		if (outstandingCall.sequenceID > forSequanceID)
			return;
		
		// see if we have timed out already
		outstandingCall.retries++;
		if (outstandingCall.retries > RPCTimeout.timeOuts)
		{
			notifyCaller(Status.Exception, new RPCTimeout());
			return;
		}
		
		byte bits[] = outstandingCall.invokePacket.encode();
		
		// make sure that we can send the packet 
		if (bits.length <= node.maxPacketSize())
			node.send(remoteServerID, Protocol.RPC.ID, bits);
		else
		{
			notifyCaller(Status.Exception, new RPCPacketTooBig());
			return;
		}
		
		// Register for timeout and re-send message as needed
		try
		{
			Method method = Callback.getMethod("sendCall", this, new String[] { Long.class.getName() } );
			Callback cb = new Callback(method, this, new Object[] { forSequanceID });
			node.addTimeout(cb, TIMEOUT);
		}
		catch (Exception e)
		{
			// we failed to register callback. This is probably an issue with the code and it will not go away on its
			// own. Time to fail0-fast
			throw new RPCBroken("Failed to register callback", e);
		}
	}
	
	/** Returns true if this RPC Client is waiting for some call to finish. If this is true, no new calls should be
	 * made.
	 * @return True if this client is waiting for reply.
	 */
	@Override
    public boolean callPending()
	{
		return outstandingCall != null;
	}

	protected String getServerStorageName(int remoteID)
	{
		return "server-" + remoteID;
	}

	@Override
	protected void processPacket(int from, RPCPacketBase packet)
	{
		// Drop all packets that are not interesting for us
		if (from != remoteServerID)		
			return;	// not our remote
		else if (packet.action != RPCPacketBase.Action.Return)
			return;	// not return - we should not be getting anything else from server
		else if (outstandingCall == null)
		{
			// This is reasonable case. This is most likely and old re-transmit packet on the wire from server. Most
			// likely we already have processed it, so it is safe to drop it.
			node.logSynopticEvent("Got old packet from server(" + from + "). Dropping. Packet: " + packet);
			return;
		}
		
		// Now we are sure that this is our packet and that it is return packet
		RPCReturnPacket returnPacket = (RPCReturnPacket) packet;
		
		assert outstandingCall.sequenceID >= packet.sequenceID : "Protocol error. Server sent us packet with, higher " +
				" sequence ID than we were waiting. Expected: " + outstandingCall.sequenceID + ", packet: " + packet;
		
		// Figure out what we would like to do with it. We also know that at this point we are waiting for the message
		if(outstandingCall.sequenceID > packet.sequenceID)
		{
			// Again this is reasonable case. This is similar to one where outstandingCall is null. In this case it
			// means that this is old packet, but in the meantime we received new invocation request
			node.logSynopticEvent("Got old packet from server(" + from + "). Dropping. Packet: " + packet);
			return;
		} else if (outstandingCall.sequenceID == packet.sequenceID)
		{
		    // See if this is result or call pending. If this is call pending we need to make sure not to use callback
		    // count
		    if (returnPacket.status == Status.CallPending)
		        outstandingCall.retries--;
		    else
    			// Got our call - trigger callback Now it is interesting technically we can crash when we are triggering
    			// callback. However, this is callback's problem and we would not want to call it again. This is more of an
    			// issue if callback throws exception that would be handled latter.
    			notifyCaller(returnPacket.status, returnPacket.result);
		}
		
		// Last if can be ignored as it is protected by assert above. On release version we should probably just drop
		// the packet. We do not really know why the server sent us this packet, so there is not much to do. We could
		// declare RPC server down, but if this is only one rouge packet we should be able to go on.
	}

	/** Notifies caller about result. Basically marks that the call was processed and invokes callers callback.
	 * 
	 * @param status Status of the call.
	 * @param result Result of the call.
	 */
	protected void notifyCaller(Status status, Serializable result)
	{
		// We may or may not set outstandingCall to null before invoking callback. If we would not set to null and
		// callback would throw, then we would have some stale state and think that this callback was not executed.
		// For one this would mean same callback execution multiple times, for other, this would put server in
		// position where we would reject all calls as we still think we are in one.
		RemoteCall old = outstandingCall;
		outstandingCall = null;		// we do not have call more
		
		old.resultCallback.resultCallback(status, result);
	}	
}
